package org.javamap;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.Toolkit;

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Iterator;

import javax.swing.Action;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
import javax.swing.JComponent;
import javax.swing.JOptionPane;

public class JavaMapPane extends JComponent implements java.awt.print.Printable
{
//***************************  Public Constants  ****************************//
	public static final int
			MODE_TERRAIN = 0,
			MODE_ZOOMIN  = 1,
			MODE_ZOOMOUT = 2,
			MODE_RIVER   = 3,
			MODE_ROAD    = 4,
			MODE_TRAIL   = 5;
			
	private static final int 
			N_MODES      = 6;
			
//***************************  Construction  ********************************//
	public JavaMapPane()
	{
		// Draw half a hexagon:
		halfHex.moveTo(0f, 0f);                  // start at bottom left corner
		halfHex.lineTo(edgeLen*0.866f, edgeLen*0.5f); // bottom-left edge
		halfHex.lineTo(edgeLen*1.732f, 0f);			// bottom-right
		halfHex.lineTo(edgeLen*1.732f, -edgeLen);	// right
		
		// The full hex should continue the half hex:
		fullHex.append(halfHex, false);
		fullHex.lineTo(edgeLen*0.866f, -edgeLen*1.5f); // top-right
		fullHex.lineTo(0f, -edgeLen);                 // top-left
		fullHex.lineTo(0f, 0f);                       // left
		
		addMouseListener(new MapMouseAdapter());
		addMouseMotionListener(new MapMouseMotionListener());
		
		mapKey('+', "ZoomIn",  new AbstractAction() {
			public void actionPerformed(ActionEvent ae)
			{ 
				Rectangle r = getVisibleRect(); 
				increaseDepth(yToRow(r.y+r.height/2), xToCol(r.x+r.width/2, 0));
			}
		});
		mapKey('-', "ZoomOut", new AbstractAction() {
			public void actionPerformed(ActionEvent ae)
			{ 
				Rectangle r = getVisibleRect(); 
				decreaseDepth(yToRow(r.y+r.height/2), xToCol(r.x+r.width/2, 0));
			}
		});
		mapKey(KeyEvent.VK_DELETE, "DelSomething", new AbstractAction() {
			public void actionPerformed(ActionEvent ae) {
				deleteSomething();
			}
		});
		mapKey( // Note using 'z' here did not work.
				KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK), 
				"Undo",
				new AbstractAction() {
					public void actionPerformed(ActionEvent ae) {
						try {
							undo();
						} catch (Exception e) {
							JOptionPane.showMessageDialog(
									getParent(),
									"Cannot undo."
							);
							e.printStackTrace();
						}
					}
				}
		);
		mapKey(
				KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK),
				"Redo",
				new AbstractAction() {
					public void actionPerformed(ActionEvent ae) {
						try {
							redo();
						} catch (Exception e) {
							JOptionPane.showMessageDialog(
									getParent(),
									"Cannot redo."
							);
						}
					}
				}
		);

		// Note that we set the preferred size, not the actual size.  The 
		// scroll pane will set the actual size to fit the viewport.
		setPreferredSize(new Dimension(1000, 1000));
		setBackground(Color.WHITE);
		
		modeCursors[MODE_ZOOMIN] = 
				getToolkit().createCustomCursor(
						ImageManager.createImage("zoom_in32.gif"),
						new Point(7, 7),
						"zoom_in"
		);
		modeCursors[MODE_ZOOMOUT] = 
				getToolkit().createCustomCursor(
						ImageManager.createImage("zoom_out32.gif"),
						new Point(13, 13),
						"zoom_out"
		);		
		modeCursors[MODE_TERRAIN] = Cursor.getPredefinedCursor(
				Cursor.HAND_CURSOR);
		modeCursors[MODE_RIVER] = 
				getToolkit().createCustomCursor(
						ImageManager.createImage("river32.gif"),
						new Point(8, 8),
						"river"
		);		
		modeCursors[MODE_ROAD] = 
				getToolkit().createCustomCursor(
						ImageManager.createImage("road32.gif"),
						new Point(8, 8),
						"river"
		);		
		modeCursors[MODE_TRAIL] = 
				getToolkit().createCustomCursor(
						ImageManager.createImage("trail32.gif"),
						new Point(8, 8),
						"trail"
		);		
		
		setMode(MODE_TERRAIN);		
	}
	
//*****************************  Accessors  *********************************//
	public Overlay getSelectedOverlay() { return currentOverlay; }
	public void setSelectedOverlay(Overlay over)
	{
		currentOverlay = over;
		repaint();
	}

	public int getDepth() { return depth; }

	public HexMap getHexMap() { return hexMap; }
	public void setHexMap(HexMap hm)
	{
		HexMap oldHexMap = hexMap;
		hexMap = hm;
		if (hexMap != null) {
			setDepth(0, 0, 0);
			for (OverlayListener ol : overlayListeners)
				hexMap.addOverlayListener(ol);
		}
		for (File f : undoStates) {
			f.delete();
		}
		undoStates.clear();
		firePropertyChange("hexMap", oldHexMap, hexMap);
		firePropertyChange("undoAvailable", true, false);
		firePropertyChange("redoAvailable", true, false);
	}
	
	public String getImageKey() { return imageKey; }
	public void setImageKey(String imageKey) { this.imageKey = imageKey; }
	
	public int getMode() { return mode; }
	public void setMode(int mode) 
	{
		this.mode = mode;
		// Set cursor appropriately:
		setCursor(modeCursors[mode]);
		currentOverlay = null;
		repaint();
	}
	
//***************************  Public Methods  ******************************//
	public void addDepthListener(DepthListener dl)
	{
		depthListeners.add(dl);
	}
	
	public void addOverlayListener(OverlayListener ol)
	{
		overlayListeners.add(ol);
	}
	
	/**
	  * Close the current HexMap, discarding unsaved changes.
	  */
	public void closeHexMap()
	{
		setHexMap(null);
	}	
		
	public void updateRulers(Ruler horiz, Ruler vert)
	{
		String unit = "feet";
		double mapRes = 5280.0 / Hex.depthArray[getDepth()];
		if (mapRes < 1) mapRes = 1.0;
			
		horiz.setLength(5280.0, "feet");
		horiz.setIncrement(mapRes);
		horiz.setStart((int) horizPad);
		horiz.setEnd((int) 
				(1.732f * edgeLen * hexMap.getNCols(getDepth()) + horizPad)
		);

		vert.setLength(5280.0, "feet");
		vert.setIncrement(mapRes);
		vert.setStart((int) horizPad);
		vert.setEnd((int)
				(1.5f * edgeLen * hexMap.getNRows(getDepth()) +vertPad)
		);
	}
	
	public HexMap loadHexMap(File file) throws IOException
	{
		try {
			java.io.FileInputStream fis = new java.io.FileInputStream(file);
			java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis);
			HexMap h = (HexMap) ois.readObject();
			fis.close();
			return h;
		} catch (IOException ioe) { throw ioe; }
		catch (Exception e) { e.printStackTrace(); return null; }
	}		
	
	public void storeHexMap(File file) throws IOException
	{
		java.io.FileOutputStream fos = new java.io.FileOutputStream(file);
		java.io.ObjectOutputStream oos 
				= new java.io.ObjectOutputStream(fos);
		oos.writeObject(hexMap);
		fos.close();
	}
	
	public void redo() throws IOException
	{
		if (undoIndex >= undoStates.size() - 1) return; // cannot redo
		
		intSetHexMap(loadHexMap(undoStates.get(++undoIndex)));
		
		if (undoIndex >= undoStates.size() - 1)
			firePropertyChange("redoAvailable", true, false);
		if (undoIndex == 1) // undo is now available:
			firePropertyChange("undoAvailable", false, true);
	}
	
	public void undo() throws IOException
	{		
		boolean couldRedoBefore = undoIndex < undoStates.size() - 1;
				
		if (undoIndex == 0) return; // cannot undo		
		if (undoIndex == undoStates.size()) {
			// Add undo state to facilitate redo:
			addUndoState();  // will increment undoIndex
			--undoIndex;
		}
		
		// Set the map state to the contents of the 
		//  file at undoStates(undoIndex):
		intSetHexMap(loadHexMap(undoStates.get(--undoIndex)));
		
		if (!couldRedoBefore)
			firePropertyChange("redoAvailable", false, true);
		
		// property, oldvalue, newvalue:
		if (undoIndex == 0) // undo is no longer available:
			firePropertyChange("undoAvailable", true, false);
	}	
	
//*****************************  JComponent Overrides  **********************//
	/**
	  * Always returns true, because this pane is always opaque.
	  */
	public boolean isOpaque() { return true; }
	
	/**
	  * Called by {@link JComponent#paint(Graphics)}.  Should not permanently
	  *  modify the state of the Graphics object because it is used by
	  *  {@link JComponent#paintBorder(Graphics)} and 
	  *  {@link JComponent#paintChildren(Graphics)}.
	  */
	protected void paintComponent(Graphics g)
	{		
		Graphics2D g2d = (Graphics2D) g;
		AffineTransform originalTransform = g2d.getTransform();
		// Use clip bounds to reduce redundant drawing:
		g2d.getClipBounds(clipRect);
//System.err.println(clipRect);
		g.setColor(getBackground());
		g.fillRect(//0, 0, getWidth(), getHeight());
				clipRect.x, clipRect.y, clipRect.width, clipRect.height);

		g2d.setColor(Color.BLACK);
		g2d.setFont(depthFont);

		// Draw based on hex-map:
		//int odd = 0;
		int rowStart = Math.max(0, yToRow(clipRect.y)-1), 
			rowEnd = Math.min(hexMapView.getNRows(), 
				yToRow(clipRect.y+clipRect.height)+1),
			colStart = Math.max(0, xToCol(clipRect.x, 1)-1), // odd offset left
			colEnd   = Math.min(hexMapView.getNCols(),
				xToCol(clipRect.x+clipRect.width, 0)+2);
//System.err.println("rs: "+rowStart+"  re: "+rowEnd+"  cs: "+colStart+"  ce: "+colEnd);
		for (int row = rowStart; row < rowEnd; ++row) {
			int odd = row & 1;
			// This is always safe:
			g2d.setTransform(originalTransform);
			g2d.translate((1-odd)*edgeLen*0.866+horizPad,
					(row+1)*edgeLen*1.5+vertPad);
			// Draw left edge of left border hexes:
			g2d.drawLine(0, 0, 0, (int) -edgeLen);
			g2d.drawLine(0, (int) -edgeLen, (int) (edgeLen*0.866), 
					(int) (-edgeLen*1.5));
					
			// Translate to first column:
			g2d.translate(edgeLen*1.732*colStart, 0);
			for (int col = colStart; col < colEnd; ++col) {
				// Draw top row top hex half:
				if (row == 0) {
					g2d.drawLine(0, (int) -edgeLen, (int) (edgeLen*0.866), 
							(int) (-edgeLen*1.5));
					g2d.drawLine((int) (edgeLen*0.866), (int) (-edgeLen*1.5),
							(int) (edgeLen*1.732), (int) -edgeLen);
				}
				g2d.draw(halfHex);
				
				Hex h = hexMapView.getHex(row, col);
//System.err.println("("+row+','+col+") = "+h);
				Image im;
				Color bgcolor;
				//if ("unknown".equals(h.getImageKey())) {
				if (h == null) {
					//im = ((GrayHex) h).getDeepestHex().getImage();
					h = hexMapView.getDeepestHex();
					bgcolor = Color.LIGHT_GRAY;
				} else {					
					bgcolor = Color.WHITE;
				}
				
				im = h.getImage();
				prepareImage(im, this);
				int imOffX = (int) ((edgeLen*1.732 - im.getWidth(this)) / 2.0),
					imOffY = (int) -((edgeLen + im.getHeight(this)) / 2.0);
				// Note that the bgcolor doesn't appear when printing.
				g2d.drawImage(im, imOffX, imOffY, bgcolor, this);
								
				// Depth text:
				if (!paintForPrint) {
					int fw = 8, fh = 8;
					g2d.drawString(String.valueOf(h.getDepth()), 
							(int) (edgeLen*1.732 - fw), -fh);
				}
				// Translate to next column:
				g2d.translate(edgeLen*1.732, 0);
			}
			// Draw right edge upper right:
			if (odd == 0)
				g2d.drawLine((int) (-edgeLen*0.866), (int) (-edgeLen*1.5),
						0, (int) -edgeLen);
			
			//odd ^= 1;
		}
		
		// Reset to original transformation:
		g2d.setTransform(originalTransform);
		
		paintSelectedLocation(g2d);
		
		// Reset to original transformation:
		g2d.setTransform(originalTransform);

		for (Iterator<Overlay> iter = hexMap.iterateOverlays(); iter.hasNext();)
			iter.next().draw(g2d, this);

		// Reset to original transformation:
		g2d.setTransform(originalTransform);
	}
	
	private void paintSelectedLocation(Graphics2D g2d)
	{
		if (selectedLoc.depth == -1) return;
		
		Stroke oldStroke = g2d.getStroke();
		g2d.setStroke(selectedStroke);

		int row = selectedLoc.row, col = selectedLoc.col, odd = row & 1;
		g2d.translate((1-odd)*edgeLen*0.866+horizPad + edgeLen*1.732*col,
				(row+1)*edgeLen*1.5+vertPad);			
		g2d.draw(fullHex);
		
		g2d.setStroke(oldStroke);
	}
//*****************************  Printable  *********************************//
	/**
	  * Prints the map.
	  * See the <a href="http://www.apl.jhu.edu/~hall/java/Swing-Tutorial/Swing-Tutorial-Printing.html">
	  * Swing Printing Tutorial</a> for more info.
	  */
	public int print(Graphics g, java.awt.print.PageFormat pageFormat, 
			int pageIndex) 
	{
		if (pageIndex > 5) return NO_SUCH_PAGE;  //safety
		
		int w = (int) pageFormat.getImageableWidth(),
			h = (int) pageFormat.getImageableHeight();

		Dimension d = getPreferredSize();
		// Print across, then down.
		int ncols = d.width / w + 1, nrows = d.height / h + 1;
		int row = pageIndex / ncols, col = pageIndex % ncols;
		
System.err.println("["+nrows+','+ncols+"]");
System.err.println("("+row+','+col+")");
		if (row >= nrows) return NO_SUCH_PAGE;
		
		Graphics2D g2d = (Graphics2D)g;
		AffineTransform oldTrans = g2d.getTransform();
		g2d.translate(
				pageFormat.getImageableX() - col*w,
				pageFormat.getImageableY() - row*h
		);
		g2d.setClip(col*w, row*h, w, h);
System.err.println(g2d.getClipBounds());
		// Turn off double buffering
		setDoubleBuffered(false);
		try {
			paintForPrint = true; // see JComponent.isPaintingForPrint(), v1.6
			paint(g2d);
		} finally {
			// Turn double buffering back on
			setDoubleBuffered(true);
			// Restore translation
			g2d.setTransform(oldTrans);
			paintForPrint = false;
		}
		return PAGE_EXISTS;
	}

//*****************************  Private Methods  ****************************//
	/**
	  * Execute before an operation which has undo support.
      */
	private void addUndoState() 
	{
		try {
			// If we have undone some operations, remove them now that we
			//   are headed down a new path.
			for (int ii = undoStates.size() - 1; ii > undoIndex; --ii)
				undoStates.remove(ii).delete();
			
			if (undoStates.size() == MAX_UNDO) {
				// Pop one off the top:
				undoStates.remove(0).delete();
			}
					
			if (undoIndex == undoStates.size()) {
				// Current state is unstored, so store:
				File tempFile = File.createTempFile("JMap", ".undo");
				storeHexMap(tempFile);
				undoStates.add(tempFile);
			}
			if (undoIndex == 0) // signal that undo is now available:
				firePropertyChange("undoAvailable", false, true);
			++undoIndex;
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}
	
	/**
	  * Internal set hex map.
	  */
	private void intSetHexMap(HexMap newHexMap)
	{
		HexMap oldHexMap = hexMap;
		hexMap = newHexMap;
		for (OverlayListener ol : overlayListeners)
			hexMap.addOverlayListener(ol);
		repaint();
		firePropertyChange("hexMap", oldHexMap, hexMap);
	}

	private void mapKey(char key, String msg, Action a)
	{
		mapKey(KeyStroke.getKeyStroke(key), msg, a);
	}		

	/**
	  * Map a virtual key without modifiers.
	  */
	private void mapKey(int vkey, String msg, Action a)
	{
		mapKey(KeyStroke.getKeyStroke(vkey, 0), msg, a);
	}		
	
	private void mapKey(KeyStroke ks, String msg, Action a)
	{
		getInputMap(WHEN_IN_FOCUSED_WINDOW).put(ks, msg);
		getActionMap().put(msg, a);
	}
	
	/**
	  * TODO: The bottom of each hex is mapped to the next row, FIX.
	  */
	private Hex pointToHex(Point p)
	{
		// First calculate row:
		int row = yToRow((int) p.getY());
		if (row >= 0 && row < hexMapView.getNRows()) {
			// Then calculate column:
			int col = xToCol((int) p.getX(), row);
//System.err.println("("+row+','+col+')');
			
			if (col >= 0 && col < hexMapView.getNCols())
				return hexMapView.getHex(row, col);
		}
				
		throw new IllegalArgumentException("Out of area");
	}
	
	/**
	  * Converts a pixel to a Location in the current view.
	  */
	private Location pointToLocation(int x, int y, Location loc)
	{
		if (loc == null) loc = new Location();
		
		loc.row = yToRow(y);
		loc.col = xToCol(x, loc.row);
		if (loc.row == -1 || loc.col == -1 
				|| loc.row >= hexMapView.getNRows()
				|| loc.col >= hexMapView.getNCols())
			loc.row = loc.col = loc.depth = -1; // mark invalid
		else loc.depth = depth;
		return loc;
	}
	
	/**
	  * The row argument is necessary because alternate hex rows are offset.
	  */
	private int xToCol(int x, int row)
	{
		double rowLeftEdge 
				= horizPad + edgeLen*0.866 - ((row & 1) == 1?edgeLen*0.866:0);
		if (x < rowLeftEdge) return -1;
		return (int) ((x - rowLeftEdge) / (edgeLen*1.732));
	}
	
	private int yToRow(int y)
	{ 
		y -= vertPad;
		if (y < 0) return -1;
		return (int) (y / (edgeLen*1.5)); 
	}
	
	/**
	  * Returns the pixel coordinates corresponding with the specified row and
	  *  column.  If oldDepth and newDepth differ the pixel will be for 
	  *  drawing at the new depth.  If rVal is null a new Point is created.
	  */
	public Point locationToPoint(int row, int col, int oldDepth, int newDepth,
			Point rVal)
	{
		if (rVal == null) rVal = new Point();
		
		double y = (edgeLen*0.75 + row * edgeLen*1.5),
			x = ((col+1) * edgeLen*1.732);
		// Odd rows are shifted left half a hex.
		if ((row & 1) == 1) x -= edgeLen*0.866;
			
		// Scale by the difference in depths:
		if (oldDepth > newDepth) { // larger scale, shrink
			x /= Hex.depthArray[oldDepth - newDepth];
			y /= Hex.depthArray[oldDepth - newDepth];
		} else { // smaller scale, grow
			x *= Hex.depthArray[newDepth - oldDepth];
			y *= Hex.depthArray[newDepth - oldDepth];
		}
		
		// Add padding after scaling:
		x += horizPad;
		y += vertPad;
		
		rVal.y = (int) y; 
		rVal.x = (int) x;
			
		return rVal;
	}
	
	public Point locationToPoint(Location loc, int newDepth, Point rVal)
	{
		return locationToPoint(loc.row, loc.col, loc.depth, newDepth, rVal);
	}
	
	/**
	  * Change the viewing depth of this pane, centering the display so that
	  *  row and col are visible.
	  */
	private void setDepth(int depth, int row, int col)
	{
		if (depth >= 0 && depth < Hex.MAXDEPTH) {
			this.depth = depth;
			setPreferredSize(new Dimension(
					(int) (hexMap.getNCols(depth)*edgeLen*1.732 + horizPad*2),
					(int) (hexMap.getNRows(depth)*edgeLen*1.5 + vertPad*2)
			));
			// Paint us:
			repaint(getVisibleRect());
			// Update scroll bars:
			revalidate();
			final Rectangle r = getVisibleRect();
			int xc = (int) (col*edgeLen*1.732 + horizPad),
				yc = (int) (row*edgeLen*1.5   + vertPad);
			r.x = xc - r.width/2;
			r.y = yc - r.height/2;
//System.err.println("Scrolling to view: "+r); // ("+xc+','+yc+')');			
//System.err.println("Before:            "+getVisibleRect());			
			// TODO: limit x,y to within preferred size.
			//scrollRectToVisible(r); //new Rectangle(xc, yc, 0, 0));
			javax.swing.SwingUtilities.invokeLater(new Runnable() {
				public void run() { scrollRectToVisible(r); }
			});
//System.err.println("After:             "+getVisibleRect());			
			notifyDepthListeners();
		}
	}
	
	private void notifyDepthListeners()
	{
		for (Iterator iter = depthListeners.iterator(); iter.hasNext(); ) {
			DepthListener dl = (DepthListener) iter.next();
			dl.depthChanged(this);
		}
	}
	
	private void setTerrain(Point p)
	{ 
		// First calculate row:
		int row = yToRow((int) p.getY());
		if (row >= 0 && row < hexMapView.getNRows()) {
			// Then calculate column:
			int col = xToCol((int) p.getX(), row); 
System.err.println("("+row+','+col+','+depth+')');
			if (col >= 0 && col < hexMapView.getNCols()) {
				addUndoState();
				hexMap.createHexes(row, col, depth).setImageKey(imageKey);
				//revalidate();
				repaint(getVisibleRect());
				return;
			}
		}
		
		throw new IllegalArgumentException("Out of bounds.");		
	}
	
	/**
	  * Zoom out, centering on the given hex's parent.
	  */
	private void decreaseDepth(int row, int col)
	{
		setDepth(depth-1, row / Hex.NCHILDPER, col / Hex.NCHILDPER);
	}
	
	private void increaseDepth(int row, int col)
	{
		int off = Hex.NCHILDPER / 2;
		setDepth(depth+1, row * Hex.NCHILDPER + off, col * Hex.NCHILDPER + off);
	}
	
	/** 
	  * Response to a delete keypress or similar.  What is deleted depends 
	  *  upon the current context.
	  */
	private void deleteSomething()
	{
		addUndoState();
		if (currentOverlay != null) {
			int selInd = currentOverlay.getSelectedIndex();
			if (selInd != -1) {
				currentOverlay.removeLocation(selInd);
				currentOverlay.selectLocation(-1);
				repaint();
			} else { // delete entire overlay
				hexMap.removeOverlay(currentOverlay);
				currentOverlay = null;
			}
		}
	}
/*	
	private void createRiver()
	{
		Object response = JOptionPane.showInputDialog(
				this,
				"Enter a name for the river:",
				"New River",
				JOptionPane.PLAIN_MESSAGE,
				new javax.swing.ImageIcon(ImageManager.createImage("river.gif")),
				null,
				null				
		);
		if (response != null) {
			addUndoState();
			currentOverlay = new org.javamap.overlays.River();
			currentOverlay.setName(response.toString());
			hexMap.addRiver((org.javamap.overlays.River) currentOverlay);
		}
	} */

	/**
	  * @return true iff the overlay was created successfully.
	  */
	private boolean createOverlay(Class<? extends Overlay> overlayClass)
	{
		String scn = overlayClass.getSimpleName(), lscn = scn.toLowerCase();
		
		Object response = JOptionPane.showInputDialog(
				this,
				"Enter a name for the "+lscn+":",
				"New "+scn,
				JOptionPane.PLAIN_MESSAGE,
				new javax.swing.ImageIcon(
						ImageManager.createImage(lscn+".gif")),
				null,
				null				
		);
		if (response != null && response.toString().length() != 0) {
			addUndoState();
			try {
				currentOverlay = overlayClass.newInstance();
				currentOverlay.setName(response.toString());
				hexMap.addOverlay(currentOverlay);
				return true;
			} catch (Exception e) { e.printStackTrace(); }			
		}
		return false;
	}
	
	/**
	  * Add the point clicked to the overlay.
	  */
	private void addToOverlay(Point p)
	{ 
		// If this is not the first point of the overlay, add an undo point:
		if (currentOverlay.getLocationCount() != 0)
			addUndoState();
		
		// See if an existing node is in the selected hex:
		int selectedLoc = currentOverlay.findLocation(p, this);
		
		if (selectedLoc != -1)
			currentOverlay.selectLocation(selectedLoc);
		else 
			currentOverlay.addLocation(pointToLocation(p.x, p.y, null));
	}		
	
//*******************************  Inner Classes  ****************************//
	private class HexMapView
	{
		public Hex getHex(int row, int col)
		{
			return hexMap.getHex(row, col, depth, deepest);
		}
	
		public int getNRows() { return hexMap.getNRows(depth); }
		public int getNCols() { return hexMap.getNCols(depth); }
		
		public Hex getDeepestHex() { return deepest[0]; }
		
		private final Hex[] deepest = new Hex[1];
	}
	
	/** 
	  * A notification interface for changes in view depth.
	  */
	public interface DepthListener extends java.util.EventListener
	{
		public void depthChanged(JavaMapPane jmp);
	}
	
	/**
	  * Interprets mouse events.
	  */
	public class MapMouseAdapter extends MouseAdapter
	{
		private void handleOverlayClick(Class<? extends Overlay> c, Point p)
		{
			if (currentOverlay == null)
				if (!createOverlay(c)) return;
			addToOverlay(p);
			repaint();
		}
		
		public void mouseClicked(MouseEvent me)
		{
			Point p = me.getPoint();
			if (me.getButton() == me.BUTTON1) {
				switch (mode) {
					// Moved to mousePressed()
					//case MODE_TERRAIN:
					//setTerrain(p);
					//break;
					
					case MODE_RIVER:
					handleOverlayClick(org.javamap.overlays.River.class, p);
					break;

					case MODE_ROAD:
					handleOverlayClick(org.javamap.overlays.Road.class, p);
					break;
					
					case MODE_TRAIL:
					handleOverlayClick(org.javamap.overlays.Trail.class, p);
					break;
					
					case MODE_ZOOMIN:
					increaseDepth(yToRow(p.y), xToCol(p.x,0));
					break;
					
					case MODE_ZOOMOUT:
					decreaseDepth(yToRow(p.y), xToCol(p.x,0));
					break;
				}					
			} else if (me.getButton() == me.BUTTON3) { // right
				//if (me.getClickCount() == 1) {
				// Debug feature, display hex:
				System.err.println(p);
				int row = yToRow(p.y);
				int col = xToCol(p.x, row);
				System.err.print("("+row+','+col+','+depth+") = ");
				Hex h = pointToHex(p);
				System.err.print(h);
				while (h != null) 
					System.err.print(" => "+(h=h.getParent()));
				System.err.println();
			}
		}
		
		public void mouseExited(MouseEvent me)
		{
			selectedLoc.depth = -1;
			repaint(getVisibleRect());
		}

		public void mousePressed(MouseEvent me)
		{
			if (me.getButton() == me.BUTTON1 && mode == MODE_TERRAIN)
				setTerrain(me.getPoint());
		}
		
	}
	
	public class MapMouseMotionListener implements MouseMotionListener
	{
		public void mouseDragged(MouseEvent me)
		{
			// Does not work, since getButton() returns state changes:
			//if (me.getButton() != me.BUTTON1) return;
			if ((me.getModifiers() & me.BUTTON1_MASK) == 0) return;
			if (mode == MODE_TERRAIN) {
				// Paint the terrain as if clicked in every hex:
				if (computeSelectionChange(me)) {
					setTerrain(lastMouse);
				}
			}				
		}
		
		public void mouseMoved(MouseEvent me)
		{
			switch (mode) {
				case MODE_TERRAIN:
				case MODE_RIVER:
				computeSelectionChange(me);
				break;
			}
		}
		
		/**
		  * @return true if the selected hex has changed.
		  */
		private boolean computeSelectionChange(MouseEvent me)
		{
			boolean result;
			
			oldSelectedLoc.set(selectedLoc);
			pointToLocation(me.getX(), me.getY(), selectedLoc);
			if (!oldSelectedLoc.equals(selectedLoc)) {
				//repaint(); // repaints only visible
				// This was way too slow for large windows, so we
				//   compute the rectangle enclosing both the 
				//  current and previous selections:
				clip.setBounds(me.getX(), me.getY(), 0, 0);
				clip.add(lastMouse);
				clip.grow(REPAINT_PAD, REPAINT_PAD);
				repaint(clip);
				result = true;
			} else result = false;
			lastMouse.setLocation(me.getX(), me.getY());
			return result;
		}
		
		private final Point lastMouse = new Point();
		private final Rectangle clip = new Rectangle();
		
		private static final int REPAINT_PAD = (int) edgeLen * 2;
	}
	
//*******************************  Data Members  ****************************//
	private HexMap hexMap;
	/**
	  * Used to store previous states of the HexMap for undo.
	  */
	private final ArrayList<File> undoStates = new ArrayList<File>();
	private int depth, mode, undoIndex;
	private String imageKey = "forest";
	private final HexMapView hexMapView = new HexMapView();
	private final ArrayList<DepthListener> depthListeners 
			= new ArrayList<DepthListener>();
	private final ArrayList<OverlayListener> overlayListeners
			= new ArrayList<OverlayListener>();
	private Overlay currentOverlay;

	// Painting objects:
	private final GeneralPath halfHex = new GeneralPath(),
			/**
			  * used only for selected hex drawing.
			  */
			fullHex = new GeneralPath();
	private final Rectangle clipRect = new Rectangle();
	private final AffineTransform translator = new AffineTransform();
	private final Font depthFont = new Font("Monospaced", Font.PLAIN, 8);
	private final Stroke selectedStroke = new java.awt.BasicStroke(
			3.0F,
			java.awt.BasicStroke.CAP_ROUND,
			java.awt.BasicStroke.JOIN_ROUND
	);
	/**
	  * True only when printing.
	  */
	private boolean paintForPrint;
	
	// User Interface objects:
	private final Cursor[] modeCursors = new Cursor[N_MODES];
	private final Location selectedLoc = new Location(), 
			oldSelectedLoc = new Location();
	
//****************************  Class Constants  ****************************//
	private final static int MAX_UNDO = 10;
	private final static float edgeLen = 20f; // a float because of genpath
	private final static float vertPad = edgeLen*2,
			horizPad = edgeLen*2;
	
//*******************************  Statics  *********************************//
}